package interp;

import ic.LanguageException;
import ic.ast.BinaryOp;
import ic.ast.ClassType;
import ic.ast.DeclClass;
import ic.ast.DeclField;
import ic.ast.DeclLibraryMethod;
import ic.ast.DeclMethod;
import ic.ast.DeclStaticMethod;
import ic.ast.DeclVirtualMethod;
import ic.ast.Length;
import ic.ast.Literal;
import ic.ast.LocalVariable;
import ic.ast.NewArray;
import ic.ast.NewInstance;
import ic.ast.Parameter;
import ic.ast.PrimitiveType;
import ic.ast.PrimitiveType.DataType;
import ic.ast.Program;
import ic.ast.Ref;
import ic.ast.RefArrayElement;
import ic.ast.RefField;
import ic.ast.RefVariable;
import ic.ast.Statement;
import ic.ast.StaticCall;
import ic.ast.StmtAssignment;
import ic.ast.StmtBlock;
import ic.ast.StmtBreak;
import ic.ast.StmtCall;
import ic.ast.StmtContinue;
import ic.ast.StmtIf;
import ic.ast.StmtReturn;
import ic.ast.StmtWhile;
import ic.ast.This;
import ic.ast.UnaryOp;
import ic.ast.VirtualCall;
import ic.ast.Visitor;

import java.util.HashMap;
import java.util.List;
import java.util.Stack;

public class InterpreterVisitor implements Visitor {

	final private String classname;
	final private String methodname;
	final private List<String> raw_args;

	final private Stack<HashMap<String, TypedValue>> symbols = new Stack<HashMap<String, TypedValue>>();
	final private SymbolsMappingHelper su;

	private PrimitiveType return_type;

	public InterpreterVisitor(String classname, String methodname,
			List<String> raw_args) {
		this.classname = classname;
		this.methodname = methodname;
		this.raw_args = raw_args;
		su = new SymbolsMappingHelper(symbols);
	}

	public InterpreterVisitor(String fullname, List<String> raw_args) {
		this(fullname.split("\\.")[0], fullname.split("\\.")[1], raw_args);
	}

	@Override
	public Object visit(Program program) {
		for (DeclClass cd : program.getClasses()) {
			if (cd.getName().equals(classname))
				return visit(cd);
		}

		throw new LanguageException(program.getLine(), "no class of name '"
				+ classname + "' found");
	}

	@Override
	public Object visit(DeclClass icClass) {
		if (icClass.getName().equals(classname)) {
			for (DeclMethod dm : icClass.getMethods()) {
				if (dm instanceof DeclStaticMethod) {
					if (dm.getName().equals(methodname)) {
						return visit((DeclStaticMethod) dm);
					}
				}
			}
		}

		throw new LanguageException(icClass.getLine(), "no method of name '"
				+ methodname + "' found in class '" + classname + "'");
	}

	@Override
	public Object visit(DeclField field) {
		throw new LanguageException(field.getLine(),
				"no implementation for this line - out of scope for exercise");
	}

	@Override
	public Object visit(DeclVirtualMethod method) {
		throw new LanguageException(method.getLine(),
				"no implementation for this line - out of scope for exercise");
	}

	@Override
	public Object visit(DeclStaticMethod method) {
		final List<Parameter> params = method.getFormals();
		if (params.size() != raw_args.size())
			throw new LanguageException(method.getLine(),
					"different number of arguments for " + methodname);

		symbols.push(new HashMap<String, TypedValue>());

		try {
			su.assignParameterByString(params, raw_args);
		} catch (Exception e) {
			throw new LanguageException(method.getLine(), e.getMessage());
		}

		return_type = (PrimitiveType) method.getType(); // Not returning class
														// instances in this
														// exercise

		try {
			for (Statement st : method.getStatements()) {
				st.accept(this);
			}
		} catch (ReturnException ret) {
			if (return_type.getType().equals(ret.retval.value.type)) {
				return ret.retval.value.getValue();
			} else {
				throw new LanguageException(ret.getLine(),
						"return of wrong type");
			}
		}

		return null;
	}

	@Override
	public Object visit(DeclLibraryMethod method) {
		throw new LanguageException(method.getLine(),
				"no implementation for this line - out of scope for exercise");
	}

	@Override
	public Object visit(Parameter formal) {
		throw new LanguageException(formal.getLine(),
				"no implementation for this line - out of scope for exercise");
	}

	@Override
	public Object visit(PrimitiveType type) {
		throw new LanguageException(type.getLine(),
				"no implementation for this line - out of scope for exercise");
	}

	@Override
	public Object visit(ClassType type) {
		throw new LanguageException(type.getLine(),
				"no implementation for this line - out of scope for exercise");
	}

	@Override
	public Object visit(StmtAssignment assignment) {
		Ref ref = assignment.getVariable();
		Object relevant_variable_details_obj = ref.accept(this);

		if (!(relevant_variable_details_obj instanceof VariabledValue || relevant_variable_details_obj instanceof ArrayCellValue)) {
			throw new LanguageException(assignment.getLine(),
					"unable to assign to non-variable");
		}

		TypedValue assignee = (relevant_variable_details_obj instanceof VariabledValue) ? ((VariabledValue) relevant_variable_details_obj).mapped_variable
				: (ArrayCellValue) relevant_variable_details_obj;

		Object newval = assignment.getAssignment().accept(this);

		if (newval instanceof TypedValue) {
			try {
				return su.checkAndAssign(assignee, (TypedValue) newval);
			} catch (Exception e) {
				throw new LanguageException(assignment.getLine(),
						e.getMessage());
			}
		} else {
			throw new LanguageException(assignment.getLine(),
					"RHS not of assignable value");
		}
	}

	@Override
	public Object visit(StmtCall callStatement) {
		throw new LanguageException(callStatement.getLine(),
				"no implementation for this line - out of scope for exercise");
	}

	private static class ReturnValue {
		public final TypedValue value;

		ReturnValue(TypedValue value) {
			this.value = value;
		}
	}

	private static class ReturnException extends LanguageException {
		private static final long serialVersionUID = -2369421217662309339L;
		public final ReturnValue retval;

		ReturnException(int line, ReturnValue v) {
			super(line, ""); // return message overriden
			this.retval = v;
		}
	}

	@Override
	public Object visit(StmtReturn st) {
		if (return_type.getType() == DataType.VOID) {
			if (st.hasValue())
				throw new LanguageException(st.getLine(),
						"returning value in void method");
			else
				throw new ReturnException(st.getLine(), new ReturnValue(
						new TypedValue(DataType.VOID, null, 0)));
		} else {
			if (st.hasValue()) {
				throw new ReturnException(st.getLine(), new ReturnValue(
						(TypedValue) st.getValue().accept(this)));
			} else {
				throw new LanguageException(st.getLine(),
						"Empty return in where should return value");
			}
		}
	}

	@Override
	public Object visit(StmtIf ifs) {
		TypedValue cond = (TypedValue) ifs.getCondition().accept(this);
		Statement todo = null;
		if ((boolean) cond.getValue()) {
			todo = ifs.getOperation();
		} else if (ifs.hasElse()) {
			todo = ifs.getElseOperation();
		}

		if (todo != null) {
			todo.accept(this);
			return null; // if doesn't evaluate to value
		} else
			return null;
	}

	@Override
	public Object visit(StmtWhile ws) {
		while (true) {
			boolean condRes = (boolean) ((TypedValue) ws.getCondition().accept(
					this)).getValue();
			if (condRes) {
				try {
					ws.getOperation().accept(this);
				} catch (BreakException e) {
					break;
				} catch (ContinueException e) {
					continue;
				}
			} else {
				break;
			}
		}

		return null;
	}

	private static class BreakException extends LanguageException {
		private static final long serialVersionUID = 8116753540162149107L;

		public BreakException(int line) {
			super(line, "break statement while not in loop");
		}
	}

	private static class ContinueException extends LanguageException {
		private static final long serialVersionUID = -2585396942756068612L;

		public ContinueException(int line) {
			super(line, "continue statement while not in loop");
		}
	}

	@Override
	public Object visit(StmtBreak breakStatement) {
		throw new BreakException(breakStatement.getLine());
	}

	@Override
	public Object visit(StmtContinue continueStatement) {
		throw new ContinueException(continueStatement.getLine());
	}

	@Override
	public Object visit(StmtBlock sb) {
		try {
			symbols.push(new HashMap<String, TypedValue>());
			for (Statement s : sb.getStatements()) {
				s.accept(this);
			}
		} finally {
			symbols.pop();
		}
		return null;
	}

	@Override
	public Object visit(LocalVariable lv) {
		if (symbols.peek().containsKey(lv.getName()))
			throw new LanguageException(lv.getLine(),
					"unable to declare new variable with same name in scope");

		if (lv.getType() instanceof PrimitiveType) {
			PrimitiveType pt = (PrimitiveType) lv.getType();
			TypedValue val = new TypedValue(pt.getType(),
					pt.getArrayDimension());
			symbols.peek().put(lv.getName(), val);

			if (lv.isInitialized()) {
				try {
					su.checkAndAssign(val, (TypedValue) lv.getInitialValue()
							.accept(this));
				} catch (Exception e) {
					throw new LanguageException(lv.getLine(), e.getMessage());
				}
			}
			return null;
		} else {
			throw new LanguageException(lv.getLine(),
					"no class instances in this exercise");
		}
	}

	@Override
	public Object visit(RefVariable ref) {
		final String name = ref.getName();
		HashMap<String, TypedValue> map = su.lookupVariable(name);

		if (map == null)
			throw new LanguageException(ref.getLine(), "no variable of name "
					+ name);

		return new VariabledValue(map.get(name), ref);

	}

	@Override
	public Object visit(RefField location) {
		throw new LanguageException(location.getLine(),
				"no implementation for this line - out of scope for exercise");
	}

	@Override
	public Object visit(RefArrayElement l) {
		TypedValue tv = (VariabledValue) l.getArray().accept(this);
		TypedValue itv = (TypedValue) l.getIndex().accept(this);
		return new ArrayCellValue(tv.type,
				((Object[]) tv.getValue())[(int) itv.getValue()],
				(int) itv.getValue(), tv);
	}

	@Override
	public Object visit(StaticCall call) {
		throw new LanguageException(call.getLine(),
				"no implementation for this line - out of scope for exercise");
	}

	@Override
	public Object visit(VirtualCall call) {
		throw new LanguageException(call.getLine(),
				"no implementation for this line - out of scope for exercise");
	}

	@Override
	public Object visit(This thisExpression) {
		throw new LanguageException(thisExpression.getLine(),
				"this in static method");
	}

	@Override
	public Object visit(NewInstance newClass) {
		throw new LanguageException(newClass.getLine(),
				"no implementation for this line - out of scope for exercise");
	}

	@Override
	public Object visit(NewArray na) {
		TypedValue tv = (TypedValue) na.getSize().accept(this);
		int size = (int) tv.getValue();
		Object[] arr = new Object[size];
		DataType arr_type = ((PrimitiveType) na.getType()).getType();
		Object defval;
		switch (arr_type) {
		case BOOLEAN:
			defval = false;
			break;
		case INT:
			defval = 0;
			break;
		default:
			defval = null;
			break;
		}
		for (int i = 0; i < arr.length; i++) {
			arr[i] = defval;
		}
		return new SourcedValue(arr_type, arr, 1, na);
	}

	@Override
	public Object visit(Length length) {
		TypedValue val = (TypedValue) length.getArray().accept(this);
		return new TypedValue(DataType.INT, ((Object[]) val.getValue()).length,
				0);
	}

	@Override
	public Object visit(Literal literal) {
		try {
			return new SourcedValue(literal.getType(),
					SymbolsMappingHelper.convertStringByType(
							(String) literal.getValue(), literal.getType()), 0,
					literal);
		} catch (Exception e) {
			throw new LanguageException(literal.getLine(), e.getMessage());
		}
	}

	@Override
	public Object visit(UnaryOp op) {
		Object res = op.getOperand().accept(this);
		if (!(res instanceof TypedValue))
			throw new LanguageException(op.getLine(),
					"no operandable expression");

		TypedValue tv = (TypedValue) res;
		switch (op.getOperator()) {
		case LNEG:
			// No type checking:
			return new SourcedValue(DataType.BOOLEAN,
					!((boolean) tv.getValue()), 0, op);
		case UMINUS:
			return new SourcedValue(DataType.INT, -((int) tv.getValue()), 0, op);
		default:
			throw new LanguageException(op.getLine(),
					"no implementation for this line - out of scope for exercise");
		}
	}

	@Override
	public Object visit(BinaryOp op) {
		Object res_l = op.getFirstOperand().accept(this);
		if (!(res_l instanceof TypedValue))
			throw new LanguageException(op.getLine(),
					"no operandable expression");

		Object res_r = op.getSecondOperand().accept(this);
		if (!(res_r instanceof TypedValue))
			throw new LanguageException(op.getLine(),
					"no operandable expression");

		TypedValue tv_l = (TypedValue) res_l;
		TypedValue tv_r = (TypedValue) res_r;
		switch (op.getOperator()) {
		case DIVIDE:
			if ((Integer) tv_l.getValue() == 0)
				throw new LanguageException(op.getLine(),
						"unable to divide by zero");
			return new SourcedValue(DataType.INT, (Integer) tv_l.getValue()
					/ (Integer) tv_r.getValue(), 0, op);
		case GT:
			return new SourcedValue(DataType.BOOLEAN,
					(Integer) tv_l.getValue() > (Integer) tv_r.getValue(), 0,
					op);
		case GTE:
			return new SourcedValue(DataType.BOOLEAN,
					(Integer) tv_l.getValue() >= (Integer) tv_r.getValue(), 0,
					op);
		case LAND:
			return new SourcedValue(DataType.BOOLEAN, (Boolean) tv_l.getValue()
					&& (Boolean) tv_r.getValue(), 0, op);
		case LOR:
			return new SourcedValue(DataType.BOOLEAN, (Boolean) tv_l.getValue()
					|| (Boolean) tv_r.getValue(), 0, op);
		case LT:
			return new SourcedValue(DataType.BOOLEAN,
					(Integer) tv_l.getValue() < (Integer) tv_r.getValue(), 0,
					op);
		case LTE:
			return new SourcedValue(DataType.BOOLEAN,
					(Integer) tv_l.getValue() <= (Integer) tv_r.getValue(), 0,
					op);
		case MINUS:
			return new SourcedValue(DataType.INT, (Integer) tv_l.getValue()
					- (Integer) tv_r.getValue(), 0, op);
		case MOD:
			return new SourcedValue(DataType.INT, (Integer) tv_l.getValue()
					% (Integer) tv_r.getValue(), 0, op);
		case MULTIPLY:
			return new SourcedValue(DataType.INT, (Integer) tv_l.getValue()
					* (Integer) tv_r.getValue(), 0, op);
		case EQUAL:
			switch (tv_l.type) {
			case BOOLEAN:
			case INT:
				return new SourcedValue(DataType.BOOLEAN, tv_l.getValue()
						.equals(tv_r.getValue()), 0, op);
			default:
				return new SourcedValue(DataType.BOOLEAN,
						tv_l.getValue() == tv_r.getValue(), 0, op);
			}
		case NEQUAL:
			switch (tv_l.type) {
			case BOOLEAN:
			case INT:
				return new SourcedValue(DataType.BOOLEAN, !tv_l.getValue()
						.equals(tv_r.getValue()), 0, op);
			default:
				return new SourcedValue(DataType.BOOLEAN,
						tv_l.getValue() != tv_r.getValue(), 0, op);
			}
		case PLUS:
			switch (tv_l.type) {
			case INT:
				return new SourcedValue(DataType.INT, (Integer) tv_l.getValue()
						+ (Integer) tv_r.getValue(), 0, op);
			case STRING:
				return new SourcedValue(DataType.STRING,
						(String) tv_l.getValue() + (String) tv_r.getValue(), 0,
						op);
			default:
				throw new LanguageException(op.getLine(),
						"no implementation for this line - out of scope for exercise");
			}
		default:
			throw new LanguageException(op.getLine(),
					"no implementation for this line - out of scope for exercise");
		}
	}

}
